package edu.northwestern.cbits.purple_robot_manager.probes.features; import java.util.ArrayList; import java.util.HashMap; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.DatabaseUtils; import android.os.Build; import android.os.Bundle; import android.provider.CallLog; import android.support.v4.content.ContextCompat; import android.telephony.PhoneNumberUtils; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; public class CallHistoryFeature extends Feature { private static final String TOTAL = "TOTAL"; private static final String OUTCOMING_COUNT = "OUTGOING_COUNT"; private static final String STRANGER_COUNT = "STRANGER_COUNT"; private static final String ACQUAINTANCE_RATIO = "ACQUAINTANCE_RATIO"; private static final String AVG_DURATION = "AVG_DURATION"; private static final String INCOMING_COUNT = "INCOMING_COUNT"; private static final String ACQUAINTANCE_COUNT = "ACQUIANTANCE_COUNT"; private static final String INCOMING_RATIO = "INCOMING_RATIO"; private static final String TOTAL_DURATION = "TOTAL_DURATION"; private static final String MAX_DURATION = "MAX_DURATION"; private static final String STD_DEVIATION = "STD_DEVIATION"; private static final String MIN_DURATION = "MIN_DURATION"; private static final String WINDOW_SIZE = "WINDOW_SIZE"; private static final String WINDOWS = "WINDOWS"; private static final String ACK_COUNT = "ACK_COUNT"; private static final String NEW_COUNT = "NEW_COUNT"; private static final String ACK_RATIO = "ACK_RATIO"; private static final String CONTACT_ANALYSES = "CONTACT_ANALYSES"; private static final String IS_ACQUAINTANCE = "IS_ACQUAINTANCE"; private static final String IDENTIFIER = "IDENTIFIER"; private long _lastCheck = 0; @Override public String getPreferenceKey() { return "features_call_history"; } @Override protected String featureKey() { return "call_history"; } @Override public String name(Context context) { return "edu.northwestern.cbits.purple_robot_manager.probes.features.CallHistoryFeature"; } @Override public String title(Context context) { return context.getString(R.string.title_call_history_feature); } @Override public String summary(Context context) { return context.getString(R.string.summary_call_history_feature_desc); } @Override public String probeCategory(Context context) { return context.getResources().getString(R.string.probe_personal_info_category); } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean("config_feature_call_history_enabled", true); e.commit(); } @Override @SuppressWarnings("deprecation") public boolean isEnabled(final Context context) { final SharedPreferences prefs = Probe.getPreferences(context); final long now = System.currentTimeMillis(); if (super.isEnabled(context)) { if (prefs.getBoolean("config_feature_call_history_enabled", true)) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, "android.permission.READ_CALL_LOG") == PackageManager.PERMISSION_GRANTED) { synchronized (this) { if (now - this._lastCheck > 15 * 60 * 1000) { this._lastCheck = now; try { boolean doHash = prefs.getBoolean("config_probe_call_hash_data", true); Bundle bundle = new Bundle(); bundle.putString("PROBE", this.name(context)); bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000); ArrayList<Bundle> analyses = new ArrayList<>(); double[] periods = { 0.25, 0.5, 1.0, 4.0, 12.0, 24.0, 168.0 }; for (double period : periods) { Bundle analysis = new Bundle(); HashMap<String, ArrayList<ContentValues>> contacts = new HashMap<>(); long start = now - ((long) Math.floor(period * 1000 * 60 * 60)); double total = 0; double incomingCount = 0; double acquaintanceCount = 0; double ackCount = 0; ArrayList<Double> durations = new ArrayList<>(); String selection = "date > ?"; String[] selectionArgs = { "" + start }; Cursor cursor = context.getContentResolver().query(CallLog.Calls.CONTENT_URI, null, selection, selectionArgs, null); while (cursor != null && cursor.moveToNext()) { total += 1; for (int i = 0; i < cursor.getColumnCount(); i++) { String name = cursor.getColumnName(i); Object value = cursor.getString(i); if (name.equals(CallLog.Calls.TYPE)) { int type = cursor.getInt(i); if (type == CallLog.Calls.INCOMING_TYPE) incomingCount += 1; } else if (name.equals(CallLog.Calls.DURATION)) durations.add((double) cursor.getInt(i)); else if (name.equals(CallLog.Calls.CACHED_NAME)) { if (value == null) { } else acquaintanceCount += 1; } else if (name.equals(CallLog.Calls.NEW)) { if (cursor.getInt(i) != 0) ackCount += 1; } } String number = PhoneNumberUtils.formatNumber(cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER))); if (doHash) number = EncryptionManager.getInstance().createHash(context, number); ContentValues phoneCall = new ContentValues(); DatabaseUtils.cursorRowToContentValues(cursor, phoneCall); ArrayList<ContentValues> calls = contacts.get(number); if (calls == null) { calls = new ArrayList<>(); contacts.put(number, calls); } calls.add(phoneCall); } if (cursor != null) cursor.close(); if (total > 0) { analysis.putDouble(CallHistoryFeature.TOTAL, total); analysis.putDouble(CallHistoryFeature.INCOMING_COUNT, incomingCount); analysis.putDouble(CallHistoryFeature.OUTCOMING_COUNT, total - incomingCount); analysis.putDouble(CallHistoryFeature.INCOMING_RATIO, incomingCount / total); analysis.putDouble(CallHistoryFeature.ACQUAINTANCE_COUNT, acquaintanceCount); analysis.putDouble(CallHistoryFeature.STRANGER_COUNT, total - acquaintanceCount); analysis.putDouble(CallHistoryFeature.ACQUAINTANCE_RATIO, acquaintanceCount / total); analysis.putDouble(CallHistoryFeature.ACK_COUNT, ackCount); analysis.putDouble(CallHistoryFeature.NEW_COUNT, total - ackCount); analysis.putDouble(CallHistoryFeature.ACK_RATIO, ackCount / total); double maxDuration = Double.MIN_VALUE; double minDuration = Double.MAX_VALUE; double totalDuration = 0; double[] primitives = new double[durations.size()]; for (int k = 0; k < durations.size(); k++) { Double duration = durations.get(k); primitives[k] = duration; totalDuration += duration; if (maxDuration < duration) maxDuration = duration; if (minDuration > duration) minDuration = duration; } StandardDeviation sd = new StandardDeviation(); double stdDev = sd.evaluate(primitives); analysis.putDouble(CallHistoryFeature.TOTAL_DURATION, totalDuration); analysis.putDouble(CallHistoryFeature.AVG_DURATION, totalDuration / total); analysis.putDouble(CallHistoryFeature.MIN_DURATION, minDuration); analysis.putDouble(CallHistoryFeature.MAX_DURATION, maxDuration); analysis.putDouble(CallHistoryFeature.STD_DEVIATION, stdDev); ArrayList<Bundle> contactBundles = new ArrayList<>(); for (String key : contacts.keySet()) { Bundle contactInfo = new Bundle(); ArrayList<ContentValues> calls = contacts.get(key); contactInfo.putString(CallHistoryFeature.IDENTIFIER, key); contactInfo.putDouble(CallHistoryFeature.TOTAL, calls.size()); contactInfo.putBoolean(CallHistoryFeature.IS_ACQUAINTANCE, calls.get(0).containsKey(CallLog.Calls.CACHED_NAME)); total = 0.0; totalDuration = 0; double incoming = 0.0; double acked = 0.0; durations = new ArrayList<>(); for (ContentValues call : calls) { try { total += 1; if (call.getAsInteger(CallLog.Calls.TYPE) == CallLog.Calls.INCOMING_TYPE) incoming += 1; if (call.getAsInteger(CallLog.Calls.NEW) == 0) acked += 1; double duration = call.getAsDouble(CallLog.Calls.DURATION); durations.add(duration); totalDuration += duration; } catch (NullPointerException e) { } } contactInfo.putDouble(CallHistoryFeature.INCOMING_COUNT, incoming); contactInfo.putDouble(CallHistoryFeature.OUTCOMING_COUNT, total - incoming); contactInfo.putDouble(CallHistoryFeature.INCOMING_RATIO, incoming / total); contactInfo.putDouble(CallHistoryFeature.ACK_COUNT, acked); contactInfo.putDouble(CallHistoryFeature.NEW_COUNT, total - acked); contactInfo.putDouble(CallHistoryFeature.ACK_RATIO, acked / total); totalDuration = 0; maxDuration = Double.MIN_VALUE; minDuration = Double.MAX_VALUE; primitives = new double[durations.size()]; for (int k = 0; k < durations.size(); k++) { Double duration = durations.get(k); primitives[k] = duration; totalDuration += duration; if (maxDuration < duration) maxDuration = duration; if (minDuration > duration) minDuration = duration; } sd = new StandardDeviation(); stdDev = sd.evaluate(primitives); contactInfo.putDouble(CallHistoryFeature.TOTAL_DURATION, totalDuration); contactInfo.putDouble(CallHistoryFeature.AVG_DURATION, totalDuration / total); contactInfo.putDouble(CallHistoryFeature.MIN_DURATION, minDuration); contactInfo.putDouble(CallHistoryFeature.MAX_DURATION, maxDuration); contactInfo.putDouble(CallHistoryFeature.STD_DEVIATION, stdDev); contactBundles.add(contactInfo); } analysis.putParcelableArrayList(CallHistoryFeature.CONTACT_ANALYSES, contactBundles); } else { analysis.putDouble(CallHistoryFeature.TOTAL, 0); analysis.putDouble(CallHistoryFeature.INCOMING_COUNT, 0); analysis.putDouble(CallHistoryFeature.OUTCOMING_COUNT, 0); analysis.putDouble(CallHistoryFeature.INCOMING_RATIO, 0); analysis.putDouble(CallHistoryFeature.ACQUAINTANCE_COUNT, 0); analysis.putDouble(CallHistoryFeature.STRANGER_COUNT, 0); analysis.putDouble(CallHistoryFeature.ACQUAINTANCE_RATIO, 0); analysis.putDouble(CallHistoryFeature.ACK_COUNT, 0); analysis.putDouble(CallHistoryFeature.NEW_COUNT, 0); analysis.putDouble(CallHistoryFeature.ACK_RATIO, 0); analysis.putDouble(CallHistoryFeature.TOTAL_DURATION, 0); analysis.putDouble(CallHistoryFeature.AVG_DURATION, 0); analysis.putDouble(CallHistoryFeature.MIN_DURATION, 0); analysis.putDouble(CallHistoryFeature.MAX_DURATION, 0); analysis.putDouble(CallHistoryFeature.STD_DEVIATION, 0); } analysis.putLong(CallHistoryFeature.WINDOW_SIZE, now - start); analyses.add(analysis); } bundle.putParcelableArrayList(CallHistoryFeature.WINDOWS, analyses); this.transmitData(context, bundle); } catch (SecurityException e) { LogManager.getInstance(context).logException(e); } } } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) SanityManager.getInstance(context).addPermissionAlert(this.name(context), "android.permission.READ_CALL_LOG", context.getString(R.string.rationale_history_call_log_probe), null); return true; } } return false; } @Override public String summarizeValue(Context context, Bundle bundle) { ArrayList<Bundle> windows = bundle.getParcelableArrayList(CallHistoryFeature.WINDOWS); Bundle window = windows.get(windows.size() - 1); double avgDuration = window.getDouble(CallHistoryFeature.AVG_DURATION); double total = window.getDouble(CallHistoryFeature.TOTAL); double stdDev = window.getDouble(CallHistoryFeature.STD_DEVIATION); return String.format(context.getResources().getString(R.string.summary_call_history_probe), total, avgDuration, stdDev); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean("config_feature_call_history_enabled", false); e.commit(); } }